昨天我們學會了測試結構與組織,但隨著測試越寫越多,你可能遇到一個問題:「為什麼這個測試單獨執行會通過,但和其他測試一起執行時會失敗?」
想像一個場景:你為數學工具庫新增了一個 CalculatorWithHistory
類別,它會記錄計算歷史。第一個測試執行時歷史是空的,測試通過;但第二個測試執行時,歷史裡已經有第一個測試留下的資料,導致測試失敗。這就是「測試污染」問題。
今天我們要學習「測試生命週期」,了解如何在每個測試執行前後進行適當的設置和清理,讓每個測試都能在乾淨、一致的環境中執行。
今天結束後,你將學會:
第一階段:打好基礎(Day 1-10)
├── Day 01 - 環境設置與第一個測試
├── Day 02 - 認識斷言(Assertions)
├── Day 03 - TDD 紅綠重構循環
├── Day 04 - 測試結構與組織
├── Day 05 - 測試生命週期 ★ 今天在這裡
├── ...
└── (更多精彩內容待續)
每個測試的執行都會經過以下階段:
設置階段 → 執行階段 → 斷言階段 → 清理階段
(Setup) (Execute) (Assert) (Cleanup)
讓我們先看看沒有適當生命週期管理的測試:
// 問題:測試之間會互相影響
let calculator = new CalculatorWithHistory()
it('add operation', () => {
const result = calculator.add(2, 3)
expect(result).toBe(5)
expect(calculator.getHistory()).toHaveLength(1)
})
it('multiply operation', () => {
const result = calculator.multiply(4, 5)
expect(result).toBe(20)
expect(calculator.getHistory()).toHaveLength(1) // ❌ 實際是 2!
})
第二個測試會失敗,因為計算器的歷史記錄還保留著第一個測試的資料。
測試隔離是指每個測試案例都應該:
// ✅ 好的測試:每個測試都是獨立的
describe('CalculatorWithHistory', () => {
let calculator: CalculatorWithHistory
beforeEach(() => {
calculator = new CalculatorWithHistory()
})
it('add operation', () => {
const result = calculator.add(2, 3)
expect(result).toBe(5)
expect(calculator.getHistory()).toHaveLength(1)
})
it('multiply operation', () => {
const result = calculator.multiply(4, 5)
expect(result).toBe(20)
expect(calculator.getHistory()).toHaveLength(1) // 現在會通過
})
})
beforeEach
是在每個測試案例執行「之前」都會執行的函數,用來設置測試環境。
建立 src/math/calculatorWithHistory.ts
:
export class CalculatorWithHistory {
private history: Array<{
operation: string
operands: number[]
result: number
}> = []
add(a: number, b: number): number {
const result = a + b
this.recordOperation('add', [a, b], result)
return result
}
multiply(a: number, b: number): number {
const result = a * b
this.recordOperation('multiply', [a, b], result)
return result
}
getHistory() {
return [...this.history]
}
getLastResult(): number | null {
return this.history.length > 0 ? this.history[this.history.length - 1].result : null
}
clearHistory(): void {
this.history = []
}
private recordOperation(operation: string, operands: number[], result: number): void {
this.history.push({ operation, operands, result })
}
}
建立 tests/day05/calculator-lifecycle.test.ts
:
import { describe, it, expect, beforeEach } from 'vitest'
import { CalculatorWithHistory } from '../../src/math/calculatorWithHistory'
describe('CalculatorWithHistory', () => {
let calculator: CalculatorWithHistory
beforeEach(() => {
// 每個測試開始前都創建一個全新的計算器
calculator = new CalculatorWithHistory()
})
describe('basic_operations', () => {
it('performs_addition', () => {
const result = calculator.add(2, 3)
expect(result).toBe(5)
expect(calculator.getHistory()).toHaveLength(1)
expect(calculator.getLastResult()).toBe(5)
})
it('performs_multiplication', () => {
const result = calculator.multiply(4, 5)
expect(result).toBe(20)
expect(calculator.getHistory()).toHaveLength(1) // 現在每個測試都是乾淨的
expect(calculator.getLastResult()).toBe(20)
})
})
describe('history_functionality', () => {
it('records_single_operation', () => {
calculator.add(2, 3)
const history = calculator.getHistory()
expect(history).toHaveLength(1)
expect(history[0].operation).toBe('add')
expect(history[0].operands).toEqual([2, 3])
expect(history[0].result).toBe(5)
})
it('records_multiple_operations', () => {
calculator.add(2, 3)
calculator.multiply(4, 5)
const history = calculator.getHistory()
expect(history).toHaveLength(2)
expect(history[0].operation).toBe('add')
expect(history[1].operation).toBe('multiply')
})
it('clears_history', () => {
calculator.add(2, 3)
calculator.multiply(4, 5)
expect(calculator.getHistory()).toHaveLength(2)
calculator.clearHistory()
expect(calculator.getHistory()).toHaveLength(0)
expect(calculator.getLastResult()).toBe(null)
})
})
})
afterEach
是在每個測試案例執行「之後」都會執行的函數,用來清理測試環境。
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
describe('cleanup_examples', () => {
let resource: any
beforeEach(() => {
resource = { active: false, data: [] }
})
afterEach(() => {
// 確保每個測試後都清理資源
resource.active = false
resource.data = []
})
it('uses_resource_properly', () => {
resource.active = true
resource.data.push('test')
expect(resource.active).toBe(true)
expect(resource.data).toHaveLength(1)
})
})
始終保持設置(setup)和清理(cleanup)的對稱性:
describe('resource_management', () => {
let resource: SomeResource
beforeEach(() => {
resource = new SomeResource()
resource.initialize()
})
afterEach(() => {
resource.cleanup()
})
})
今天我們深入學習了測試生命週期的重要概念:
測試生命週期是確保測試穩定性和可靠性的基礎。通過適當的設置和清理:
記住:良好的測試生命週期管理是可靠測試的基石。
明天我們將學習「參數化測試」,了解如何用同一個測試邏輯驗證多組不同的資料,讓測試更加高效和全面。